---
title: UCS03 - ZKGM
---

import Mermaid from "#/components/Mermaid.astro";

`ucs03-zkgm-0` is the most advanced and recommended protocol to use for

- message passing
- asset transfers
- intents
- storage proofs

It's the most gas-efficient version and suitable for almost all use cases.

## Protocol

### Open Filling

A groundbreaking protocol improvement on IBC and trust-minimized bridging in general, is that `ucs03-zkgm-0` allows arbitrary filling of orders by any party.

:::tip

Read more on filling and counterparty risk [here](/concepts/filling).

:::

For packet submissions and transfers, the protocol allows a different filler from the Union IBC contracts. In the case of an alternative filler, the assets
are not minted but transferred from the filler's account. This allows a transfer to be filled before chains have been finalized or client updates processed. Instead,
fillers can rely on preconfirmations to reduce the risk of reorgs.

<Mermaid
  content={`
sequenceDiagram
    Chain A ->> Chain B: Packet (via Filler)
    Chain B ->> Chain A: Acknowledgement
    note over Chain A,Chain B: Filler can act before finality
`}/>

Theoretically, a filler can submit the transfer to the destination side before the transaction is included on the source, given that they protect themselves against double-spend attacks.

The `Acknowledgement`, which may contain arbitrary payloads, is used to encode information on the filler and repay the filler for the service by unlocking assets from the vault.

Open filling is opt-in for any protocol, allowing for the same optimizations that `ucs03-zkgm-0` leverages to increase transfer speeds.

### Rate Limiting

ZKGM incorporates rate limiting capabilities through a token bucket mechanism to manage token consumption:

- Each token has a configurable capacity and refill rate
- Transactions are rate-limited based on token amounts
- Rate limiting can be toggled on/off for specific deployment needs
- Administrators can adjust bucket parameters as needed

### Immutable Packet

A fundamental design principle of the ZKGM protocol is that packets are fully constructed off-chain and remain immutable once sent. Unlike traditional cross-chain messaging protocols, ZKGM never injects or modifies fields during on-chain processing—it only verifies the packet's integrity and processes it as submitted.

This immutability offers several powerful capabilities:

1. **Mempool Inspection and Intent Filling**: Market makers can observe pending transactions in the mempool and identify ZKGM packets before they're included in a block. This enables near-instant filling of orders, reducing latency to the theoretical minimum.

2. **Deterministic Multi-Chain Execution**: The packet's immutability combined with deterministic salt derivation creates a verifiable and predictable execution chain. When a packet contains a Forward instruction:

   - The next packet's salt is derived deterministically: `deriveForwardSalt(salt) = tintForwardSalt(keccak256(salt))`
   - This derived salt is used for the next hop's packet
   - Each subsequent forwarded packet's salt is similarly derived
   - The entire chain of packet execution becomes deterministic and verifiable

   This determinism allows market makers to:
   - Predict and validate the entire multi-chain execution path
   - Simulate the outcome across all chains before execution
   - Execute packets atomically across chains with confidence
   - Verify the authenticity of packets in a chain without requiring on-chain storage

#### Use Cases

**Flash Loans Across Chains**: A trader can construct a sequence of packets representing a complex cross-chain arbitrage:
- Packet 1: Borrow assets on Chain A
- Packet 2: Execute swap on Chain B
- Packet 3: Repay loan on Chain A with profit

A market maker can detect these packets in the mempool, validate the profitability, and execute all steps atomically, effectively enabling cross-chain flash loans.

**Cross-Chain Limit Orders**: A user can create a packet representing a limit order with specific execution parameters. Market makers can monitor the mempool and execute only when market conditions match the user's requirements, providing decentralized cross-chain limit order capabilities.

**Preemptive Bridging**: Services can monitor user interactions with dApps and predict upcoming cross-chain transfers. By the time the user initiates the transfer, the assets can already be pre-positioned on the destination chain through intent filling, creating an instant bridging experience.


## Definition

The zkgm protocol abstracts away multiple facets of IBC assets transfer protocol (ics20).
We employ versioning in this protocol to ensure backward compatibility with future upgrades (not relying on the IBC channel upgrade feature).
Each instruction has a version and opcode to allow for protocol evolution.
Its features include:
- batching
- forward/callback envelopes
- channel routing
- fungible asset transfers

## Packet

The zkgm protocol uses two main structures for packet handling:

### ZkgmPacket

```solidity
struct ZkgmPacket {
    bytes32 salt;          // Unique packet identifier
    uint256 path;          // Channel routing information
    Instruction instruction; // The instruction to execute
}
```

Fields:
- `salt`: A unique `bytes32` identifier used for deterministic packet hashing
  - For regular packets: `keccak256(abi.encodePacked(sender, user_provided_salt))`
  - For forwarded packets: Uses a tinting mechanism to track packet chain
    - Magic value: `0xC0DE00000000000000000000000000000000000000000000000000000000BABE`
    - Tinting applied as: `salt | magic_value` (bitwise OR)
    - previous_salt is the salt of the packet being forwarded
    - Next hop salt derived as: `keccak256(previous_salt) | magic_value`
    - This creates a verifiable chain of salts across hops while preventing salt collisions
  - For batch instructions: Each sub-instruction uses `keccak256(index, batch_salt)`
    - Where index is the instruction's position in the batch (0-based)
    - And batch_salt is the parent packet's salt

- `path`: A `uint256` that tracks packet routing and asset origins
  - Composed of compressed `uint32` channel IDs in 32-bit segments
  - Format: `prevDstChannel0 | nextSrcChannel0 << 32 | prevDstChannel1 << 64 ...`
  - Supports up to 3 hops (256/32/2 - 1), one hop is a placeholder for the final channel ID.
  - Updated during:
    - Packet forwarding (appending channel pairs)
    - Asset origin tracking
    - Return path validation

- `instruction`: The [Instruction](#instruction) to execute

:::tip
The `salt` replaces the traditional IBC sequence number for packet identification. This allows:
- Unique packet identification without maintaining sequence state on-chain
- Deterministic computation of future packet hashes for filling (e.g., for forwarded packets)
- Prevention of packet collisions in a stateless manner
- Market Makers protection against reorgs (using preconf to have a guarantee that initiating tx is included at some point)
:::

### Instruction

```solidity
struct Instruction {
    uint8 version;    // Protocol version
    uint8 opcode;     // Instruction type
    bytes operand;    // Instruction-specific data
}
```

Fields:
- `version`: Protocol version number as `uint8`
  - Required for backward compatibility
  - Allows dispatching between different versions
  - Each instruction type specifies required version
  - Current supported versions:
    - `INSTR_VERSION_0` (0x00)
    - `INSTR_VERSION_1` (0x01)
    - `INSTR_VERSION_2` (0x02)

- `opcode`: Instruction type identifier as `uint8`
  - 0x00: [Forward](#0x00---forward)
  - 0x01: [Call](#0x01---call)
  - 0x02: [Batch](#0x02---batch)
  - 0x03: [TokenOrder](#0x03---token-order)

- `operand`: Instruction-specific data as `bytes`
  - Forward: Path, timeouts and instruction to forward
  - Call: Sender, callback mode and contract call data
  - Batch: Array of instructions to execute atomically
  - TokenOrder: Transfer details like tokens, amounts and parties

:::note

The `version` field is embedded in every instruction. One may think that this value is redundant for recursive packets. The reality is that upgrade propagation may take time (optimistic rollups for instance) and having every instruction specifying their version will allow to dispatch from a higher version to a lower one. This is specifically true for packet forwarding (chain **A** running version `X` forwarding to a chain **B** running version `Y`).

:::


## Instructions

### 0x00 - Forward

The forward instruction uses opcode `0x00` and requires version `INSTR_VERSION_0`.

```solidity
struct Forward {
    uint256 path;              // Channel sequence as (prevDst,nextSrc) pairs
    uint64 timeoutHeight;      // Block height timeout
    uint64 timeoutTimestamp;   // Unix timestamp timeout
    Instruction instruction;    // Instruction to forward
}
```

Fields:
- `path`: A `uint256` that encodes the forwarding route
  - Composed of (prevDst,nextSrc) channel ID pairs
  - Each pair uses 64 bits (32 bits per channel ID)
  - Must match valid channel connections
  - Used to verify packet routing path

- `timeoutHeight`: Block height timeout as `uint64`
  - After this height, packet cannot be executed
  - Set to 0 to disable height timeout
  - Must be greater than current height when executed

- `timeoutTimestamp`: Unix timestamp timeout as `uint64`
  - After this time, packet cannot be executed
  - Set to 0 to disable timestamp timeout
  - Must be greater than current time when executed

- `instruction`: The [Instruction](#instruction) to forward
  - Can be Call, TokenOrder, or Batch
  - Will be executed on final destination chain
  - Cannot be another Forward instruction

:::tip

The forward instruction can wrap Call, TokenOrder, or Batch instructions. The path parameter encodes pairs of (previous destination channel, next source channel) for routing. For example, to forward through a single hop, you need both the previous destination channel ID and the next source channel ID that form the connection between chains.

The protocol verifies that only these specific instructions can be forwarded and that the channel pairs are valid.

:::

<Mermaid
  content={`
sequenceDiagram
    Chain A ->> Chain B: Forward Packet
    Chain B ->> Chain C: Original Packet
    Chain C ->> Chain B: Acknowledgement
    Chain B ->> Chain A: Forward Acknowledgement
`}/>

### 0x01 - Call

The call instruction uses opcode `0x01` and requires version `INSTR_VERSION_0`.

```solidity
struct Call {
    bytes sender;            // Source chain sender address (must match msg.sender)
    bool eureka;            // Whether to use IBC-style callbacks
    bytes contractAddress;   // Target contract address on destination
    bytes contractCalldata;  // Call data for the target contract
}
```

Fields:
- `sender`: Source chain sender address as `bytes`
  - Must match transaction sender (msg.sender)
  - Prevents address impersonation
  - Used for callback routing in eureka mode

- `eureka`: Callback mode flag as `bool`
  - false: Standard fire-and-forget mode
  - true: IBC-style callback mode
  - Determines target contract interface
  - Controls acknowledgement handling

- `contractAddress`: Target contract address as `bytes`
  - Must be valid contract on destination chain
  - Must implement required interface based on eureka flag
  - Where message will be delivered

- `contractCalldata`: Contract call data as `bytes`
  - Arbitrary data passed to target contract
  - Interpreted by target contract's implementation
  - Available in both standard and eureka modes

The Call instruction has two modes:

1. Standard Mode (eureka = false):
   - Target must implement IZkgmable interface
   - Calls `onZkgm(path, sourceChannel, destChannel, sender, calldata)` on target
   - Returns success acknowledgement immediately
   - Fire-and-forget style, no callback to sender
   - **ProxyAccount Auto-Creation**: If the target address matches a predictable ProxyAccount address derived from the sender, path, and channel, the ProxyAccount is automatically created before the call is executed

2. IBC Mode (eureka = true):
   - Calls `onRecvPacket(packet, relayer, relayerMsg)` on target
   - Packet contains path, sender and calldata
   - Target must return non-empty acknowledgement
   - Acknowledgement forwarded back to original sender

If the target contract is invalid or calls fail:
- Standard mode returns failure acknowledgement
- IBC mode propagates target's error response

#### ProxyAccount

ProxyAccount is a smart contract wallet that enables cross-chain account abstraction. It allows users to control accounts on remote chains through the ZKGM protocol.

**Key Features:**
- **Deterministic Addresses**: ProxyAccount addresses are derived deterministically from the sender address, channel ID and path, ensuring the same sender always gets the same ProxyAccount address
- **Auto-Creation**: When a Call instruction targets a ProxyAccount address that doesn't exist yet, the protocol automatically deploys it before executing the call
- **Multi-Admin Support**: Supports both local admins (on the same chain) and remote admins (on other chains)
- **Upgradeable**: Implements UUPS pattern for upgradeability on EVM
- **Multicall Support**: Can execute multiple calls in a single transaction

**Address Prediction:**
```solidity
function predictProxyAccount(
    uint256 path,
    uint32 channel,
    bytes calldata sender
) external returns (address, bytes32);
```

The ProxyAccount address is computed as:
1. Salt = `keccak256(abi.encode(path, channelId, sender))`
2. Address = `CREATE3.predictDeterministicAddress(salt)`

**Remote Control:**
When called via zkgm (eureka = false), the ProxyAccount:
1. Verifies the caller is the authorized zkgm contract
2. Checks if the sender is a registered remote admin for the given path/channel
3. Decodes the message as `(address target, uint256 value, bytes payload)`
4. Executes the call to the target contract

**Example Usage:**
```solidity
// On source chain, prepare a call to be executed via ProxyAccount
bytes memory proxyCalldata = abi.encode(
    targetContract,     // Contract to call on destination
    0,                  // ETH value to send
    abi.encodeCall(     // The actual function call
        IERC20.transfer,
        (recipient, amount)
    )
);

// Prepare a call instruction to submit.
Call memory call = Call({
    sender: abi.encodePacked(msg.sender),
    eureka: false,
    contractAddress: abi.encodePacked(proxyAccountAddress),
    contractCalldata: proxyCalldata
});
```

This enables users to:
- Own and control accounts on any chain without deploying contracts in advance
- Execute arbitrary transactions on remote chains
- Manage assets and interact with DeFi protocols cross-chain
- Maintain consistent account addresses across deployments

:::tip
Example of a contract implementing `IZkgmable`:
```solidity
contract MyContract is IZkgmable {
    event MessageReceived(
        uint256 path,
        uint32 sourceChannelId,
        uint32 destinationChannelId,
        address sender,
        bytes message
    );

    function onZkgm(
        uint256 path,
        uint32 sourceChannelId,
        uint32 destinationChannelId,
        bytes calldata sender,
        bytes calldata message
    ) external {
        // Verify caller is zkgm contract
        require(msg.sender == address(zkgm), "Only zkgm");

        // Verify that the sourceChannelId and sender are authorized...

        // Process the cross-chain message
        emit MessageReceived(
            path,
            sourceChannelId,
            destinationChannelId,
            address(bytes20(sender)),
            message
        );
    }
}
```
:::

<Mermaid
  content={`
sequenceDiagram
    participant X as Contract X
    participant B as Chain B
    participant Y as Contract Y

    X ->> B: Call Message
    alt IS NOT EUREKA
        B ->> Y: onZkgm()
        B ->> X: Success Ack
    else IS EUREKA
        B ->> Y: onRecvPacket()
        Y ->> B: Custom Ack
        B ->> X: Custom Ack
    end
`}/>

### 0x02 - Batch

The batch instruction uses opcode `0x02` and requires version `INSTR_VERSION_0`.

```solidity
struct Batch {
    Instruction[] instructions;  // Array of instructions to execute
}
```

Fields:
- `instructions`: Array of [Instructions](#instruction) to execute atomically
  - Only specific instructions allowed (Call, TokenOrder)
  - Executed in sequential order
  - All must succeed or entire batch reverts
  - Individual acknowledgements collected in array
  - Minimum 2 instructions required

This allows atomic composition of transfers and contract calls in a single transaction.

<Mermaid
  content={`
sequenceDiagram
    Chain A ->> Chain B: Batch [p₀, p₁, ...]
    activate Chain B
    Chain B ->> Chain B: Execute All Packets
    Chain B ->> Chain A: Batch Acknowledgement
    deactivate Chain B
`}/>

:::tip

In combination with forward envelopes, this batching mechanism is very useful to call contract before/after message execution (transfer an asset, swap it, then execute another operation with the final asset).

:::

### 0x03 - Token Order

The token order instruction has two versions:

1. Version 1 (INSTR_VERSION_1) - **DEPRECATED**:

```solidity
struct TokenOrderV1 {
    bytes sender;             // Source chain sender address
    bytes receiver;           // Destination chain receiver address
    bytes baseToken;          // Token being sent
    uint256 baseAmount;       // Amount being sent
    string baseTokenSymbol;   // Token symbol for wrapped asset
    string baseTokenName;     // Token name for wrapped asset
    uint8 baseTokenDecimals;  // Token decimals for wrapped asset
    uint256 baseTokenPath;    // Origin path for unwrapping
    bytes quoteToken;         // Token requested in return
    uint256 quoteAmount;      // Minimum amount requested
}
```

2. Version 2 (INSTR_VERSION_2):

```solidity
struct TokenOrderV2 {
    bytes sender;             // Source chain sender address
    bytes receiver;           // Destination chain receiver address
    bytes baseToken;          // Token being sent
    uint256 baseAmount;       // Amount being sent
    bytes quoteToken;         // Token requested in return
    uint256 quoteAmount;      // Minimum amount requested
    uint8 kind;               // Type of metadata (initialize, escrow, unescrow, solve)
    bytes metadata;           // Token metadata or solver metadata based on type
}
```

#### TokenOrderV2: Advanced Token Mapping and Customization

The V2 version introduces a significantly more flexible metadata system with four types:
- `TOKEN_ORDER_KIND_INITIALIZE` (0x00): Provides full metadata implementation and initializer for custom token deployment
```solidity
struct TokenMetadata {
    bytes implementation;    // Implementation contract address
    bytes initializer;       // Initialization data for proxy
}
```
- `TOKEN_ORDER_KIND_ESCROW` (0x01): Uses a metadata image hash for existing token identification
- `TOKEN_ORDER_KIND_UNESCROW` (0x02): Specifically for unwrapping operations
- `TOKEN_ORDER_KIND_SOLVE` (0x03): Routes the order to a solver contract for custom fulfillment logic
```solidity
struct SolverMetadata {
    bytes solverAddress;     // Address of the solver contract
    bytes metadata;          // Additional metadata for the solver
}
```

The key innovation in V2 is the ability to support 1:N token mappings, allowing the same source token to be represented by different implementations on the destination chain. This enables several powerful use cases:

1. **Custom Token Implementations**: Projects can map a token to their own implementation with specific features or behaviors. For example, a project could map USDC to a custom token that includes additional functionality like rebasing or built-in protocol-specific mechanics.

2. **Upgradeability Management**: Tokens can be deployed with specific upgradeability patterns chosen by the implementing protocol rather than being fixed to a single implementation pattern.

3. **Enhanced Security Controls**: Custom implementations can include additional security features like transfer limits, allowlists, or compliance mechanisms tailored to specific regulatory requirements.

This flexibility is achieved by allowing the specification of:
- A custom implementation contract address
- Custom initialization data
- A deterministic salt based on the metadata

Similarly to V1, in V2 the protocol uses CREATE3 to deploy the token contracts at deterministic addresses, ensuring that the same token metadata always results in the same deployed address.

Common Fields (both versions):
- `sender`: Source chain sender address as `bytes`
  - Must be valid address on source chain
  - Used for refunds on failure/timeout

- `receiver`: Destination chain receiver address as `bytes`
  - Must be valid address on destination chain
  - Where quote tokens will be sent on success
  - Must be specified by sender

- `baseToken`: Token being sent as `bytes`
  - Token identifier on source chain
  - Used to identify/create wrapped token
  - Must exist on source chain

- `baseAmount`: Amount being sent as `uint256`
  - Must be available from sender
  - Maximum amount to exchange

- `quoteToken`: Requested token as `bytes`
  - Token identifier on destination chain
  - What sender wants in exchange
  - Must exist on destination chain

- `quoteAmount`: Minimum amount requested as `uint256`
  - Minimum acceptable exchange amount
  - Difference (if less than `baseAmount`) is taken as fee by the relayer

The order can be filled in three ways:

1. Protocol Fill - If the quote token matches the wrapped version of the base token and base amount >= quote amount:
   - For new assets: Deploy wrapped token contract and mint quote amount to receiver
   - For returning assets: Unwrap base token and transfer quote amount to receiver
   - Any difference between baseAmount and quoteAmount is minted/transferred to the relayer as a fee
   - Rate limiting may be applied based on configuration

2. Market Maker Fill - Any party can fill the order by providing the quote token:
   - Market maker is specified in acknowledgement
   - Base token is transferred/minted to market maker
   - Market maker must handle quote token transfer on behalf of the protocol

3. Solver Fill - Smart contracts that implement the solver interface can programmatically fill orders:
   - Solver addresses are encoded in the order metadata when `kind == TOKEN_ORDER_KIND_SOLVE` (0x03)
   - The `quoteToken` field specifies what token the solver should provide to the receiver
   - Solvers receive full context: packet, order, path, caller, relayer, relayerMsg, and intent flag
   - Solvers can implement complex logic like multi-hop swaps, arbitrage, or liquidity aggregation
   - On solver failure, the protocol returns `ACK_ERR_ONLYMAKER` to allow other market makers to fill
   - Solvers use the same `FILL_TYPE_MARKETMAKER` acknowledgment type as regular market makers
   - The solver must return the market maker address as `bytes` to receive the base tokens (typically a preconfigured beneficiary address controlled by the solver, not the relayerMsg)
   - The address is returned as `bytes` to support different chain address formats (e.g., 20-byte Ethereum addresses, Cosmos bech32 addresses, etc.)

The acknowledgement includes:
- Fill type (Protocol = `0xB0CAD0` or MarketMaker = `0xD1CEC45E`)
- Market maker address for MM fills (empty for protocol fills)

If the order fails or times out:
- For new assets: Base token is minted back to sender
- For returning assets: Base token is transferred back to sender
- Outstanding balances are decreased

#### Solver Interface

TokenOrderV2 introduces support for solver contracts - smart contracts that can programmatically fill orders with custom logic. This enables sophisticated market-making strategies beyond simple token transfers.

**Solidity Implementation:**

```solidity
interface ISolver {
    function solve(
        IBCPacket calldata packet,
        TokenOrderV2 calldata order,
        uint256 path,
        address caller,
        address relayer,
        bytes calldata relayerMsg,
        bool intent
    ) external returns (bytes memory);
}
```

Solver contracts must:
1. Implement the `ISolver` interface
2. Handle the `solve()` function call with all order context
3. Return the market maker address (as `bytes`) that will receive the base tokens on the source chain
   - Must return `bytes` to support different address formats across chains
   - For example, Ethereum addresses are 20 bytes, but other chains may use different formats

**CosmWasm Implementation:**

```rust
pub enum SolverMsg {
    DoSolve {
        packet: Packet,
        order: CwTokenOrderV2,
        path: Uint256,
        caller: Addr,
        relayer: Addr,
        relayer_msg: Bytes,
        intent: bool,
    },
}
```

CosmWasm solvers must:
1. Implement the `SolverMsg::DoSolve` execute handler
2. Perform the necessary token transfers or logic
3. Emit a `solver` event with `market_maker` attribute containing the recipient address

**Solver Invocation:**

When an order has `kind == TOKEN_ORDER_KIND_SOLVE` (0x03):
- **Solidity**: The protocol decodes `SolverMetadata` from the order's metadata field to extract the solver address
- **CosmWasm**: The protocol extracts the solver address from the metadata and invokes `SolverMsg::DoSolve`
- The solver contract is called with the full order context
- The solver must return the market maker address that will receive the base tokens
- In CosmWasm, the market maker address is communicated via a `solver` event with a `market_maker` attribute

**Execution Flow:**

When an order has `kind == TOKEN_ORDER_KIND_SOLVE`:
1. The protocol extracts the solver address from the `SolverMetadata` in the order's metadata field
2. The solver's `solve()` function is called with full context including the path parameter
3. The solver performs its custom logic and returns the market maker address to receive base tokens
4. On success, a market maker acknowledgment is returned with the solver-provided address
5. On failure or if no market maker address is provided, `ACK_ERR_ONLYMAKER` is returned, allowing other market makers to attempt filling

**Use Cases:**

- **Multi-hop Swaps**: Solvers can execute complex swap routes across multiple DEXs
- **Liquidity Aggregation**: Combine liquidity from multiple sources for better rates
- **Arbitrage**: Execute arbitrage opportunities while fulfilling user orders
- **Custom Logic**: Implement protocol-specific market making strategies
- **Cross-chain Coordination**: Coordinate actions across multiple chains

**Important Implementation Detail:**

Solvers typically configure different beneficiary addresses per path/channel/token combination. This allows them to:
- Route base tokens to different vaults or addresses on different chains
- Manage liquidity separately for different trading pairs
- Coordinate with counterparty contracts on the source chain

**Example Solver Implementation:**

```solidity
contract MultiDEXSolver is ISolver {
    // Beneficiary addresses configured per path/channel/token
    // Using bytes to support addresses from different chains (not just 20-byte addresses)
    mapping(uint256 => mapping(uint32 => mapping(bytes => bytes))) public beneficiaries;

    struct DEXParams {
        uint256 maxSlippage;
        address[] routePath;
        uint256 deadline;
    }

    function solve(
        IBCPacket calldata packet,
        TokenOrderV2 calldata order,
        uint256 path,
        address caller,
        address relayer,
        bytes calldata relayerMsg,
        bool intent
    ) external override returns (bytes memory) {
        require(!intent, "only finalized txs are currently supported");

        // Decode the SolverMetadata from the order
        SolverMetadata memory solverMeta = abi.decode(order.metadata, (SolverMetadata));

        // Decode custom parameters from the solver metadata's metadata field
        DEXParams memory params = abi.decode(solverMeta.metadata, (DEXParams));

        // Validate deadline
        require(block.timestamp <= params.deadline, "Order expired");

        // The quoteToken from the order specifies what token to provide
        address quoteToken = address(bytes20(order.quoteToken));
        address receiver = address(bytes20(order.receiver));

        // Validate that we support this quote token
        require(isSupported(quoteToken), "Unsupported quote token");

        // Perform the trade to obtain the quote tokens
        uint256 outputAmount = findBestRouteAndTrade(
            order.baseToken,
            order.baseAmount,
            quoteToken,
            order.quoteAmount,
            params
        );

        // Ensure minimum output is met
        require(outputAmount >= order.quoteAmount, "Insufficient output");

        // Transfer the quote tokens to the receiver
        IERC20(quoteToken).transfer(receiver, order.quoteAmount);

        // Keep any excess as profit
        uint256 profit = outputAmount - order.quoteAmount;
        if (profit > 0) {
            IERC20(quoteToken).transfer(relayer, profit);
        }

        // Return the market maker address that should receive the base tokens
        // This is typically configured per path/channel/token combination
        bytes memory beneficiary = beneficiaries[path][packet.destinationChannelId][order.baseToken];
        require(beneficiary.length > 0, "No beneficiary configured");
        return beneficiary;
    }
}
```

#### Token Order Kinds: Protocol Rules and Solver Communication

The `kind` field in TokenOrderV2 serves two critical purposes: **protocol-level validation** and **solver communication**. Understanding when and how to use each kind is essential for proper protocol operation.

##### Solver Fill via TOKEN_ORDER_KIND_SOLVE

When `kind == TOKEN_ORDER_KIND_SOLVE` (0x03), the order is routed to a solver contract instead of being processed by the protocol or regular market makers. This enables sophisticated custom fulfillment logic:

1. **Metadata Structure**: The order's `metadata` field must contain an encoded `SolverMetadata` struct:
   - `solverAddress`: The address of the solver contract to invoke
   - `metadata`: Additional arbitrary data for the solver to use

2. **Routing**: The protocol extracts the solver address from the metadata and calls the solver's `solve()` function

3. **Return Value**: The solver must return the market maker address that should receive the base tokens on the source chain

4. **Fallback**: If the solver fails or doesn't return a valid address, `ACK_ERR_ONLYMAKER` is returned, allowing other market makers to attempt filling

##### Protocol Validation Rules

The protocol enforces strict validation rules based on the token order kind:

**TOKEN_ORDER_KIND_INITIALIZE (0x00)**:
- **Required when**: Deploying a new wrapped token with custom metadata
- **Metadata**: Must contain valid `TokenMetadata` with implementation contract and initializer data
- **Validation**: Protocol verifies the wrapped token doesn't already exist
- **Use case**: First-time token bridging with custom token contracts

**TOKEN_ORDER_KIND_ESCROW (0x01)**:
- **Required when**: Sending tokens that will be escrowed (locked) on the source chain to receive quote tokens on the destination
- **Metadata**: Can contain arbitrary data for solver interpretation
- **Validation**: Protocol checks if the token has a V1 (metadata image = 0) or V2 (non-zero metadata image) representation
- **Use case**: Regular token transfers where base tokens are locked on source chain

**TOKEN_ORDER_KIND_UNESCROW (0x02)**:
- **Required when**: Unwrapping tokens back to their original form
- **Metadata**: Can contain arbitrary data for solver interpretation
- **Validation**: Protocol verifies the token is being sent back through its origin path
- **Use case**: Returning wrapped tokens to their source chain

##### Solver Communication Channel

When using `TOKEN_ORDER_KIND_SOLVE`, the `SolverMetadata` structure enables powerful communication between users and solvers:

**Arbitrary Metadata Threading**:
```solidity
// Example: User wants specific slippage tolerance for a solver
struct SlippageParams {
    uint256 maxSlippage;
    uint256 deadline;
    string preferredDEX;
}

// Encode custom parameters for the solver
SolverMetadata memory solverMeta = SolverMetadata({
    solverAddress: abi.encodePacked(address(customSolver)),
    metadata: abi.encode(SlippageParams({
        maxSlippage: 300, // 3%
        deadline: block.timestamp + 3600,
        preferredDEX: "uniswap"
    }))
});

TokenOrderV2 memory order = TokenOrderV2({
    sender: abi.encodePacked(msg.sender),
    receiver: abi.encodePacked(recipient),
    baseToken: abi.encodePacked(token),
    baseAmount: amount,
    quoteToken: abi.encodePacked(desiredOutputToken), // Token the solver should provide
    quoteAmount: minOutput,
    kind: TOKEN_ORDER_KIND_SOLVE,
    metadata: abi.encode(solverMeta)
});
```

**Solver Interpretation**:
```solidity
contract CustomSolver is ISolver {
    // Beneficiary addresses configured per path/channel
    // Using bytes to support addresses from different chains
    mapping(uint256 => mapping(uint32 => bytes)) public beneficiaries;

    struct SlippageParams {
        uint256 maxSlippage;
        uint256 deadline;
        string preferredDEX;
    }

    function solve(
        IBCPacket calldata packet,
        TokenOrderV2 calldata order,
        uint256 path,
        address caller,
        address relayer,
        bytes calldata relayerMsg,
        bool intent
    ) external override returns (bytes memory) {
        // First decode the SolverMetadata from the order
        SolverMetadata memory solverMeta = abi.decode(order.metadata, (SolverMetadata));

        // Then decode user preferences from the solver metadata's metadata field
        SlippageParams memory params = abi.decode(solverMeta.metadata, (SlippageParams));

        // Use the metadata to customize execution
        if (keccak256(bytes(params.preferredDEX)) == keccak256("uniswap")) {
            executeUniswapTrade(order, params);
        } else {
            executeBestRouteTrade(order, params);
        }

        // Return the market maker address to receive base tokens
        // Lookup the beneficiary for this specific path/channel
        bytes memory beneficiary = beneficiaries[path][packet.destinationChannelId];
        require(beneficiary.length > 0, "No beneficiary for this route");
        return beneficiary;
    }
}
```

##### Advanced Solver Use Cases

**1. Creating a Solver Order**:
```solidity
// First, encode the solver metadata
SolverMetadata memory solverMeta = SolverMetadata({
    solverAddress: abi.encodePacked(address(mySolverContract)),
    metadata: abi.encode(CustomSolverParams({
        maxSlippage: 300,  // 3%
        deadline: block.timestamp + 3600,
        preferredDEX: "uniswap",
        minLiquidity: 100000e6
    }))
});

// Create the order with TOKEN_ORDER_KIND_SOLVE
TokenOrderV2 memory order = TokenOrderV2({
    sender: abi.encodePacked(msg.sender),
    receiver: abi.encodePacked(recipient),
    baseToken: abi.encodePacked(USDC),
    baseAmount: 10000e6,
    quoteToken: abi.encodePacked(address(uToken)), // U token that solver will mint
    quoteAmount: 9900e6,  // Minimum U tokens expected
    kind: TOKEN_ORDER_KIND_SOLVE,  // 0x03
    metadata: abi.encode(solverMeta)
});
```

**2. Complex Solver Logic with Multi-Step Execution**:
```solidity
struct MultiStepParams {
    uint8 steps;
    address[] intermediateTokens;
    uint256[] minOutputs;
    bytes[] customData;
}

// Create a solver order with multi-step instructions
SolverMetadata memory solverMeta = SolverMetadata({
    solverAddress: abi.encodePacked(address(multiStepSolver)),
    metadata: abi.encode(MultiStepParams({
        steps: 3,
        intermediateTokens: [USDC, WETH, TARGET_TOKEN],
        minOutputs: [1000e6, 0.5e18, 100e18],
        customData: [swapParams1, swapParams2, swapParams3]
    }))
});

TokenOrderV2 memory order = TokenOrderV2({
    sender: abi.encodePacked(msg.sender),
    receiver: abi.encodePacked(recipient),
    baseToken: abi.encodePacked(USDC),
    baseAmount: 1000e6,
    quoteToken: abi.encodePacked(TARGET_TOKEN), // Final token solver should provide
    quoteAmount: 100e18, // Minimum TARGET_TOKEN output expected
    kind: TOKEN_ORDER_KIND_SOLVE,
    metadata: abi.encode(solverMeta)
});
```

**3. Conditional Execution Logic**:
```solidity
struct ConditionalParams {
    uint256 triggerPrice;
    bool triggerAbove;
    uint256 maxDelay;
    bytes fallbackAction;
}

// Solver can implement limit order logic
SolverMetadata memory solverMeta = SolverMetadata({
    solverAddress: abi.encodePacked(address(limitOrderSolver)),
    metadata: abi.encode(ConditionalParams({
        triggerPrice: 2000e18, // Execute only if ETH > $2000
        triggerAbove: true,
        maxDelay: 3600,
        fallbackAction: abi.encode("refund")
    }))
});

TokenOrderV2 memory order = TokenOrderV2({
    sender: abi.encodePacked(msg.sender),
    receiver: abi.encodePacked(recipient),
    baseToken: abi.encodePacked(WETH),
    baseAmount: 1e18,
    quoteToken: abi.encodePacked(USDC), // USDC that solver should provide
    quoteAmount: 2000e6, // Minimum USDC expected
    kind: TOKEN_ORDER_KIND_SOLVE,
    metadata: abi.encode(solverMeta)
});
```
##### Metadata Usage Guidelines

**For Regular Token Orders (non-solver)**:
- `TOKEN_ORDER_KIND_INITIALIZE`: `metadata` contains `TokenMetadata` with implementation and initializer
- `TOKEN_ORDER_KIND_ESCROW`: `metadata` can be empty or contain protocol-specific data (not for custom execution)
- `TOKEN_ORDER_KIND_UNESCROW`: `metadata` can be empty or contain protocol-specific data (not for custom execution)

**For Solver Orders**:
- `TOKEN_ORDER_KIND_SOLVE`: `metadata` must contain `SolverMetadata` with:
  - `solverAddress`: The solver contract to invoke
  - `metadata`: Arbitrary data for the solver to interpret

##### Best Practices

**For Protocol Compliance**:
1. Always use `TOKEN_ORDER_KIND_UNESCROW` when unwrapping tokens
2. Use `TOKEN_ORDER_KIND_INITIALIZE` only for first deployment with custom metadata
3. Use `TOKEN_ORDER_KIND_ESCROW` for regular transfers (without solver logic)
4. Use `TOKEN_ORDER_KIND_SOLVE` when routing to a solver contract

**For Solver Integration**:
1. When creating orders for solvers, set `kind = TOKEN_ORDER_KIND_SOLVE` (0x03)
2. Encode the solver address and any additional metadata in `SolverMetadata` struct
3. Place the encoded `SolverMetadata` in the order's metadata field
4. Ensure your solver returns a valid market maker address to receive base tokens
5. Validate all inputs in your solver's `solve()` function
6. Provide clear error messages when solver execution fails
8. In CosmWasm, emit a `solver` event with `market_maker` attribute containing the recipient address

**Security Considerations**:
1. Solvers should validate all metadata fields before execution
2. Never trust metadata without proper validation
3. Implement bounds checking for numeric parameters
4. Consider gas costs when decoding complex metadata structures
5. Provide clear error messages when metadata validation fails

This dual-purpose design makes TokenOrderV2 both protocol-compliant and highly extensible, enabling sophisticated use cases while maintaining security and predictability.

<Mermaid
  content={`
sequenceDiagram
    participant S as Sender
    participant A as Chain A
    participant B as Chain B
    participant MM as Market Maker
    participant Sol as Solver Contract

    S ->> A: Submit Transfer Order
    A ->> B: Asset Transfer Order
    alt Protocol Fill
        B ->> B: Verify & Mint/Transfer
        B ->> A: Protocol Fill Ack
    else Market Maker Fill
        MM ->> B: Fill Order with Quote Token
        B ->> A: MM Fill Ack
        A ->> MM: Transfer Base Token
    else Solver Fill
        B ->> Sol: solve(packet, order, context)
        Sol ->> Sol: Execute Custom Logic
        Sol ->> B: Return Market Maker Address
        B ->> A: MM Fill Ack with Solver's MM Address
        A ->> MM: Transfer Base Token to Solver's Market Maker
    end
`}/>

## EVM Token Order Examples

Below are practical EVM examples for each TokenOrderV2 operation type, extracted from the canonical test suite.

### Initialize Operation (TOKEN_ORDER_KIND_INITIALIZE)

The initialize operation creates a new wrapped token with custom metadata on the destination chain.

#### Basic Token Initialization

```solidity

// Default ZkgmERC20 address, don't change this unless you know what you are doing.
bytes memory implementation = abi.encodePacked(0xAf739F34ddF951cBC24fdbBa4f76213688E13627);

// Default ZkgmERC20.initialize (could be your implementation.initialize)
// If you don't need any customization, you'll just update the token name, symbol and decimals.
bytes memory initializer =
    abi.encodeCall(
            ZkgmERC20.initialize,
            (   // Default authority address, don't change this unless you know what you are doing.
                0x40cdff51ae7487e0b4a4d6e5f86eb15fb7c1d9f4,
                // Default zkgm address, don't change this unless you know what you are doing.
                0x5fbe74a283f7954f10aa04c2edf55578811aeb03,
                // Token name
                "My Token",
                // Token symbol
                "MTK",
                // Decimals
                18
            )
        );

// Create metadata for the new wrapped token
TokenMetadata memory metadata = TokenMetadata({
    implementation: abi.encodePacked(0xAf739F34ddF951cBC24fdbBa4f76213688E13627),
    initializer: initialize
});

// Predict the wrapped token address using the metadata
// Use cast to call the counterparty zkgm instance:
// cast call <COUNTERPARTY_ZKGM_ADDRESS> "predictWrappedTokenV2(uint256,uint32,bytes,tuple(bytes,bytes))" \
//   0 <DESTINATION_CHANNEL_ID> <BASE_TOKEN> "(<IMPLEMENTATION>,<INITIALIZER>)" \
//   --rpc-url <COUNTERPARTY_RPC_URL>

// Create the token order
TokenOrderV2 memory order = TokenOrderV2({
    sender: abi.encodePacked(msg.sender),
    receiver: abi.encodePacked(recipient),
    baseToken: baseToken,
    baseAmount: 1000e18,
    kind: ZkgmLib.TOKEN_ORDER_KIND_INITIALIZE,
    metadata: ZkgmLib.encodeTokenMetadata(metadata),
    // This is the predicted address we got
    quoteToken: abi.encodePacked(predictedWrappedTokenAddress),
    quoteAmount: 1000e18
});

// Create instruction
Instruction memory instruction = Instruction({
    version: ZkgmLib.INSTR_VERSION_2,
    opcode: ZkgmLib.OP_TOKEN_ORDER,
    operand: ZkgmLib.encodeTokenOrderV2(order)
});
```

### Escrow Operation (TOKEN_ORDER_KIND_ESCROW)

The escrow operation uses an existing wrapped token (created via initialize) to lock tokens on the source chain.

```solidity
// Send more of the same token using the already initialized wrapped token
TokenOrderV2 memory order = TokenOrderV2({
    sender: abi.encodePacked(msg.sender),
    receiver: abi.encodePacked(recipient),
    baseToken: baseToken, // Same base token as before
    baseAmount: 500e18,
    kind: ZkgmLib.TOKEN_ORDER_KIND_ESCROW,
    metadata: hex"", // Empty for escrow
    quoteToken: abi.encodePacked(predictedWrappedTokenAddress), // Same wrapped token from initialize
    quoteAmount: 500e18
});
```

### Unescrow Operation (TOKEN_ORDER_KIND_UNESCROW)

The unescrow operation unwraps tokens back to their original form, returning them through their origin path.

```solidity
// Unwrap the token back to its original form
TokenOrderV2 memory order = TokenOrderV2({
    sender: abi.encodePacked(msg.sender),
    receiver: abi.encodePacked(originalRecipient),
    baseToken: abi.encodePacked(predictedWrappedTokenAddress), // The wrapped token
    baseAmount: 300e18,
    kind: ZkgmLib.TOKEN_ORDER_KIND_UNESCROW,
    metadata: hex"", // Empty for unescrow
    quoteToken: baseToken, // Back to original token
    quoteAmount: 300e18
});
```


### Solve Operation (TOKEN_ORDER_KIND_SOLVE)

The solve operation routes orders to smart contracts that implement custom fulfillment logic.

```solidity
// Create solver metadata
SolverMetadata memory solverMeta = SolverMetadata({
    solverAddress: abi.encodePacked(solverContractAddress),
    metadata: hex"" // No additional parameters
});

TokenOrderV2 memory order = TokenOrderV2({
    sender: abi.encodePacked(0x95Fb5cb304508d74d855514D7bC9bDA75c304cE2),
    receiver: abi.encodePacked(0x95Fb5cb304508d74d855514D7bC9bDA75c304cE2),
    baseToken: abi.encodePacked(tokenSent),
    baseAmount: 10,
    kind: ZkgmLib.TOKEN_ORDER_KIND_SOLVE,
    metadata: ZkgmLib.encodeSolverMetadata(solverMeta),
    quoteToken: abi.encodePacked(tokenToReceive),
    quoteAmount: 10
});
```

:::tip

A user can split a big order over multiple packets to have a partial-filling like behavior.
For instance, one could split a \$100K order over 10 packets to allow different participants (usually market makers) to fill chunks of \$10K.

Read more on filling [here](/concepts/filling).

:::

:::warn

If any of the order in the `orders` list is failing on execution, the whole packet is reverted and a failure acknowledgement will be yield.

:::


## Implementations


import { LinkCard } from '@astrojs/starlight/components';


<LinkCard title="UCS03 - EVM reference implementation" description="Canonical UCS03 implementation in Solidity" href="https://github.com/unionlabs/union/blob/main/evm/contracts/apps/ucs/03-zkgm/" />

<LinkCard title="UCS03 - CosmWasm reference implementation" description="Canonical UCS03 implementation in Rust" href="https://github.com/unionlabs/union/tree/main/cosmwasm/ibc-union/app/ucs03-zkgm" />
